/**
 * Copyright 1998-2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
 * All rights reserved.
 */
package ca.uvic.cs.cogz.mapping.ui;

import java.awt.Component;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.TreePath;

import ca.uvic.cs.cogz.mapping.core.AbstractMapElement;
import ca.uvic.cs.cogz.mapping.core.MappingCollection;
import ca.uvic.cs.cogz.mapping.ontology.DataLoaderFacade;
import ca.uvic.cs.cogz.mapping.ui.renderer.ArcRenderer;
import ca.uvic.cs.cogz.mapping.ui.tree.MappingJTree;
import edu.stanford.smi.protege.model.Frame;
import edu.stanford.smi.protege.model.Instance;
import edu.stanford.smi.protege.model.Project;
import edu.stanford.smi.protege.util.LazyTreeNode;

/**
 * Mouse listener for the mapping visualization panel.
 * 
 * @author seanf
 */
public class MappingPanelMouseListener extends MouseAdapter implements MouseMotionListener, MouseWheelListener {
	private static final int MAX_MATCHES_TO_SHOW = 4;

	/**
	 * threshold for minimum acceptable distance when calculating mouse gesture move for tree
	 * expansion/collapse
	 */
	private static final int DISTANCE_THRESHOLD = 10;

	/**
	 * threshold for minimum acceptable velocity when calculating mouse gesture move for tree
	 * expansion/collapse
	 */
	private static final double VELOCITY_THRESHOLD = 0.15;

	private boolean dragMapping;

	private MappingVisualizationPanel mappingVisualizationPanel;

	/** custom panel for displaying a tooltip */
	private BalloonPanel tooltip;

	private JPanel arcsMiddleHoverPanel;

	private ArcRenderer arcRenderer;

	/** the source and target trees for the ontologies */
	private JTree sourceTree;
	private JTree targetTree;

	/** collection of mapped items */
	private MappingCollection mappingCollection;

	/** previous mouse drag location */
	private Point prevDragLocation = null;

	private AbstractMapElement currentHoveredElement;

	/** previous timestamp for the drag event */
	private long lastMouseDrag;

	public MappingPanelMouseListener(MappingVisualizationPanel mappingVisualizationPanel) {
		this.mappingVisualizationPanel = mappingVisualizationPanel;
	}

	public void initUIReferences() {
		sourceTree = mappingVisualizationPanel.getSourceTree();
		targetTree = mappingVisualizationPanel.getTargetTree();
		arcsMiddleHoverPanel = mappingVisualizationPanel.getArcsMiddleHoverPanel();
		arcRenderer = mappingVisualizationPanel.getArcRenderer();
		tooltip = mappingVisualizationPanel.getTooltip();
		mappingCollection = DataLoaderFacade.getInstance().getMappingCollection();
	}

	public boolean isDragging() {
		return dragMapping;
	}

	public void mousePressed(MouseEvent e) {
		if (SwingUtilities.isLeftMouseButton(e)) {
			// find the item under the mouse
			if (arcsMiddleHoverPanel.equals(e.getSource())) {
				mappingVisualizationPanel.selectMappingArc((Component) e.getSource(), e.getPoint(), e.isControlDown());
			} else if (sourceTree.equals(e.getSource())) {
				arcRenderer.setDragSource(sourceTree);
				startDrag(e.getPoint());
			}

			if ( e.getSource() instanceof JTree) {
				if(e.getClickCount() == 1) handleTreeSingleClick(e);
				else if(e.getClickCount() == 2) handleTreeDoubleClick(e);
			}
		}
	}

	public void mouseReleased(MouseEvent e) {
		if (dragMapping) {
			// find the tree item under the mouse
			if (sourceTree.equals(e.getSource())) {
				Point p = SwingUtilities.convertPoint(sourceTree, e.getPoint(), targetTree);
				TreePath path = targetTree.getPathForLocation(p.x, p.y);
				if (path != null) {
					mappingVisualizationPanel.addMapping(arcRenderer.getSourcePath(), path);
				} else {
					arcRenderer.setDragPoint(null);
					mappingVisualizationPanel.repaintArcsPanel(); // remove the partial arc
				}
			}

			dragMapping = false;
		}
	}

	public void mouseDragged(MouseEvent e) {
		if (dragMapping) {
			// set the drag end point - this is a point in the arcs panel
			Component source = (Component) e.getSource();

			arcRenderer.setDragPoint(SwingUtilities.convertPoint(source, e.getPoint(), mappingVisualizationPanel.getArcsPanel()));

			Point p = SwingUtilities.convertPoint(source, e.getPoint(), targetTree);
			TreePath hoveredPath = targetTree.getPathForLocation(p.x, p.y);
			if (hoveredPath != null) {
				targetTree.setSelectionPath(hoveredPath);

				handleMouseGesture(p, hoveredPath, e.getWhen());
			}

			mappingVisualizationPanel.repaintArcsPanel();
		}

	}

	public void mouseMoved(MouseEvent e) {
		Component source = (Component) e.getSource();
		List<AbstractMapElement> mapElements = null;
		AbstractMapElement mapElement = null;

		// update the balloon tooltip position
		if (source == arcsMiddleHoverPanel && arcRenderer.getSize() > 0) {
			// display the annotation as the tooltip
			Point p = SwingUtilities.convertPoint(source, e.getPoint(), mappingVisualizationPanel.getArcsPanel());
			mapElements = arcRenderer.getMapElement(p);

			if (mapElements != null && mapElements.size() > 0) {
				mapElement = mapElements.get(0);

				// make sure the mapping element is different
				if (mapElement.equals(currentHoveredElement)) {
					return;
				}

				mappingCollection.moveToFront(mapElement);
				arcRenderer.setHoveredElement(mapElements.get(0));

				//int numOfMappings = mappingCollection.getNumOfMappings(mapElements.get(0).getSourceFrame());

				currentHoveredElement = mapElement;

				// @tag note : setVisible call handles the repainting, no need for me to call it
				//if (mapElement.hasAnnotation() && (numOfMappings == ArcRenderer.ELEMENTS_FOR_COMPOSITE || mapElements.size() == 1)) {
				if (mapElement.hasAnnotation()) {
					tooltip.setText(mapElement.getAnnotation());
					tooltip.setLocation(p.x, p.y - tooltip.getHeight() / 2);
					tooltip.setVisible(true);
					mappingVisualizationPanel.repaintArcsPanel();
//				} else if (numOfMappings > ArcRenderer.ELEMENTS_FOR_COMPOSITE) {
//					tooltip.setText(getCompositeAnnotation(mapElements));
//					tooltip.setLocation(p.x, p.y - tooltip.getHeight() / 2);
//					tooltip.setVisible(true);
				} else {
					mappingVisualizationPanel.repaintArcsPanel();
				}
			} else {
				currentHoveredElement = null;
			}
		}

		// remove highlight
		if (mapElement == null && arcRenderer.getHoveredElement() != null) {
			tooltip.setVisible(false);
			arcRenderer.setHoveredElement(null);
			mappingVisualizationPanel.repaintArcsPanel();
		}
	}

	private String getCompositeAnnotation(List<AbstractMapElement> mapElements) {
		StringBuilder sbAnnotation = new StringBuilder();
		if (!arcRenderer.isExpanded(mapElements)) {
			sbAnnotation.append("Click here to view all candidates for this item.<br><b>" + mappingCollection.getNumOfMappings(mapElements.get(0).getSourceFrame()) + " candidate mappings</b>.");

			int i = 0;
			sbAnnotation.append("<table width=\"90%\" align=\"center\" cellspacing=\"0\" cellpadding=\"0\">");
			Set<Frame> seen = new HashSet<Frame>();
			int numSeen = 0;
			for (; i < mapElements.size() && numSeen < MAX_MATCHES_TO_SHOW; i++) {
				if (!seen.contains(mapElements.get(i).getTargetFrame())) {
					sbAnnotation.append("<tr><td>" + mapElements.get(i).getTargetFrame().getBrowserText() + "</td></tr>");
					numSeen++;
					seen.add(mapElements.get(i).getTargetFrame());
				}
			}
			if (i < mapElements.size()) {
				sbAnnotation.append("<tr><td>...</td></tr>");
			}
			sbAnnotation.append("</table>");
		} else {
			sbAnnotation.append("Click here to hide all candidates for this item.<br><b>" + mapElements.size() + " candidate mappings</b>.");
		}

		return sbAnnotation.toString();
	}

	/**
	 * Gets the selected path based on the event location and displays the class editor for the Cls
	 * user object stored at the associated node.
	 * 
	 * @param e
	 */
	private void handleTreeDoubleClick(MouseEvent e) {
		JTree tree = (JTree) e.getSource();
		TreePath path = tree.getPathForLocation(e.getX(), e.getY());
		if (path != null && path.getLastPathComponent() instanceof LazyTreeNode) {
			LazyTreeNode treeNode = (LazyTreeNode) path.getLastPathComponent();
			Instance instance = (Instance) treeNode.getUserObject();
			Project project;
			if (tree.equals(sourceTree)) {
				project = DataLoaderFacade.getInstance().getSourceOntology().getProject();
			} else {
				project = DataLoaderFacade.getInstance().getTargetOntology().getProject();
			}
			project.show(instance);
		}
	}
	
	private void handleTreeSingleClick(MouseEvent e) {
		JTree tree = (JTree) e.getSource();
		TreePath path = tree.getPathForLocation(e.getX(), e.getY());
		
		if (path != null && path.getLastPathComponent() instanceof LazyTreeNode) {
			//mappingVisualizationPanel.selectTreeConcept(tree, path);
		}
	}

	/**
	 * Determines whether a valid mouse gesture has occurred and then either collapses or expands
	 * the currently hovered path. A valid mouse gesture is a movement to the left (collapse) or
	 * right (expansion) at a certain velocity over a certain distance.
	 */
	private void handleMouseGesture(Point p, TreePath path, long currentTimeStamp) {
		if (prevDragLocation != null) {
			long t = currentTimeStamp - lastMouseDrag;
			double v = Math.abs(prevDragLocation.distance(p)) / t;

			try {
				if (v > VELOCITY_THRESHOLD) {
					if (p.x - prevDragLocation.x > DISTANCE_THRESHOLD) {
						targetTree.expandPath(path);
						((MappingJTree) targetTree).refreshTree();
					} else if (p.x - prevDragLocation.x < -10) {
						targetTree.collapsePath(path);
					}
				}
			} catch (NullPointerException e) { // ignore this
			}
		}

		prevDragLocation = p;
		lastMouseDrag = currentTimeStamp;
	}

	/**
	 * Starts the mouse drag when the user is manually mapping two concepts.
	 */
	private void startDrag(Point p) {
		TreePath path = arcRenderer.getDragSource().getPathForLocation(p.x, p.y);
		if (path != null) {
			arcRenderer.setSourcePath(path);
			dragMapping = true;
		}
	}

	public void mouseWheelMoved(MouseWheelEvent e) {
		for (MouseWheelListener mwl : ((MappingJTree) sourceTree).getScrollPane().getMouseWheelListeners()) {
			mwl.mouseWheelMoved(e);
		}

		for (MouseWheelListener mwl : ((MappingJTree) targetTree).getScrollPane().getMouseWheelListeners()) {
			mwl.mouseWheelMoved(e);
		}
	}
}
